/*
 * Copyright (c) Kumo Inc. and affiliates.
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <iosfwd>

#include <melon/conv.h>
#include <melon/expected.h>
#include <melon/range.h>
#include <melon/unit.h>
#include <melon/lang/bits.h>

namespace melon {
    class IPAddressV6;

    enum class MacAddressFormatError {
        Invalid,
    };

    /*
     * MacAddress represents an IEEE 802 MAC address.
     */
    class MacAddress {
    public:
        static constexpr size_t SIZE = 6;
        static const MacAddress BROADCAST;
        static const MacAddress ZERO;

        /*
         * Construct a zero-initialized MacAddress.
         */
        MacAddress() { memset(&bytes_, 0, 8); }

        /*
         * Parse a MacAddress from a human-readable string.
         * The string must contain 6 one- or two-digit hexadecimal
         * numbers, separated by dashes or colons.
         * Examples: 00:02:C9:C8:F9:68 or 0-2-c9-c8-f9-68
         */
        explicit MacAddress(StringPiece str);

        static Expected<MacAddress, MacAddressFormatError> tryFromString(
            StringPiece value) {
            MacAddress ret;
            auto ok = ret.trySetFromString(value);
            if (!ok) {
                return makeUnexpected(ok.error());
            }
            return ret;
        }

        static MacAddress fromString(StringPiece value) {
            MacAddress ret;
            ret.setFromString(value);
            return ret;
        }

        /*
         * Construct a MAC address from its 6-byte binary value
         */
        static Expected<MacAddress, MacAddressFormatError> tryFromBinary(
            ByteRange value) {
            MacAddress ret;
            auto ok = ret.trySetFromBinary(value);
            if (!ok) {
                return makeUnexpected(ok.error());
            }
            return ret;
        }

        static MacAddress fromBinary(ByteRange value) {
            MacAddress ret;
            ret.setFromBinary(value);
            return ret;
        }

        /*
         * Construct a MacAddress from a uint64_t in network byte order.
         *
         * The first two bytes are ignored, and the MAC address is taken from the
         * latter 6 bytes.
         *
         * This is a static method rather than a constructor to avoid confusion
         * between host and network byte order constructors.
         */
        static MacAddress fromNBO(uint64_t value) { return MacAddress(value); }

        /*
         * Construct a MacAddress from a uint64_t in host byte order.
         *
         * The most significant two bytes are ignored, and the MAC address is taken
         * from the least significant 6 bytes.
         *
         * This is a static method rather than a constructor to avoid confusion
         * between host and network byte order constructors.
         */
        static MacAddress fromHBO(uint64_t value) {
            return MacAddress(Endian::big(value));
        }

        /*
         * Construct the multicast MacAddress for the specified multicast IPv6
         * address.
         */
        static MacAddress createMulticast(IPAddressV6 addr);

        /*
         * Get a pointer to the MAC address' binary value.
         *
         * The returned value points to internal storage inside the MacAddress
         * object.  It is only valid as long as the MacAddress, and its contents may
         * change if the MacAddress is updated.
         */
        const uint8_t *bytes() const { return bytes_ + 2; }

        /*
         * Return the address as a uint64_t, in network byte order.
         *
         * The first two bytes will be 0, and the subsequent 6 bytes will contain
         * the address in network byte order.
         */
        uint64_t u64NBO() const { return packedBytes(); }

        /*
         * Return the address as a uint64_t, in host byte order.
         *
         * The two most significant bytes will be 0, and the remaining 6 bytes will
         * contain the address.  The most significant of these 6 bytes will contain
         * the first byte that appear on the wire, and the least significant byte
         * will contain the last byte.
         */
        uint64_t u64HBO() const {
            // Endian::big() does what we want here, even though we are converting
            // from big-endian to host byte order.  This swaps if and only if
            // the host byte order is little endian.
            return Endian::big(packedBytes());
        }

        /*
         * Return a human-readable representation of the MAC address.
         */
        std::string toString() const;

        /*
         * Update the current MacAddress object from a human-readable string.
         */
        Expected<Unit, MacAddressFormatError> trySetFromString(StringPiece value);

        void setFromString(StringPiece value);

        void parse(StringPiece str) { setFromString(str); }

        /*
         * Update the current MacAddress object from a 6-byte binary representation.
         */
        Expected<Unit, MacAddressFormatError> trySetFromBinary(ByteRange value);

        void setFromBinary(ByteRange value);

        bool isBroadcast() const { return *this == BROADCAST; }
        bool isMulticast() const { return getByte(0) & 0x1; }
        bool isUnicast() const { return !isMulticast(); }

        /*
         * Return true if this MAC address is locally administered.
         *
         * Locally administered addresses are assigned by the local network
         * administrator, and are not guaranteed to be globally unique.  (It is
         * similar to IPv4's private address space.)
         *
         * Note that isLocallyAdministered() will return true for the broadcast
         * address, since it has the locally administered bit set.
         */
        bool isLocallyAdministered() const { return getByte(0) & 0x2; }

        // Comparison operators.

        bool operator==(const MacAddress &other) const {
            // All constructors and modifying methods make sure padding is 0,
            // so we don't need to mask these bytes out when comparing here.
            return packedBytes() == other.packedBytes();
        }

        bool operator<(const MacAddress &other) const {
            return u64HBO() < other.u64HBO();
        }

        bool operator!=(const MacAddress &other) const { return !(*this == other); }

        bool operator>(const MacAddress &other) const { return other < *this; }

        bool operator>=(const MacAddress &other) const { return !(*this < other); }

        bool operator<=(const MacAddress &other) const { return !(*this > other); }

    private:
        explicit MacAddress(uint64_t valueNBO) {
            memcpy(&bytes_, &valueNBO, 8);
            // Set the pad bytes to 0.
            // This allows us to easily compare two MacAddresses,
            // without having to worry about differences in the padding.
            bytes_[0] = 0;
            bytes_[1] = 0;
        }

        template<typename OnError>
        Expected<Unit, MacAddressFormatError> setFromString(
            StringPiece value, OnError err);

        template<typename OnError>
        Expected<Unit, MacAddressFormatError> setFromBinary(
            ByteRange value, OnError err);

        /* We store the 6 bytes starting at bytes_[2] (most significant)
           through bytes_[7] (least).
           bytes_[0] and bytes_[1] are always equal to 0 to simplify comparisons.
        */
        unsigned char bytes_[8];

        inline uint64_t getByte(size_t index) const { return bytes_[index + 2]; }

        uint64_t packedBytes() const {
            uint64_t u64;
            memcpy(&u64, bytes_, 8);
            return u64;
        }
    };

    /* Define toAppend() so to<string> will work */
    template<class Tgt>
    typename std::enable_if<IsSomeString<Tgt>::value>::type toAppend(
        MacAddress address, Tgt *result) {
        toAppend(address.toString(), result);
    }

    std::ostream &operator<<(std::ostream &os, MacAddress address);
} // namespace melon

namespace std {
    // Provide an implementation for std::hash<MacAddress>
    template<>
    struct hash<melon::MacAddress> {
        size_t operator()(const melon::MacAddress &address) const {
            return std::hash<uint64_t>()(address.u64HBO());
        }
    };
} // namespace std
